/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright (c) 2008-2009, The KiWi Project (http://www.kiwi-project.eu)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice, 
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice, 
 *   this list of conditions and the following disclaimer in the documentation 
 *   and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Contributor(s):
 * 
 * 
 */
package kiwi.core.extension;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Identity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.inject.Scope;
import javax.media.j3d.Interpolator;
import javax.print.attribute.standard.Severity;
import javax.servlet.http.HttpServletRequest;

import kiwi.core.api.extension.KiWiApplication;

import org.dom4j.DocumentException;
import org.dom4j.Element;

import sun.security.validator.ValidatorException;
import sun.tools.tree.MethodExpression;

import com.google.common.base.Strings;
import com.sun.org.apache.xml.internal.security.Init;

/**
 * An extension of the JBoss Seam pages.xml component that allows to read and parse pages.xml
 * files from extensions in addition to those of the main application.
 * 
 * @author Sebastian Schaffert
 *
 */
@Scope(ScopeType.APPLICATION)
@BypassInterceptors
@Name("org.jboss.seam.navigation.pages")
@Injectstall(precedence=APPLICATION, classDependencies="javax.faces.context.FacesContext")
@Startup
public class KiWiPages extends Pages {

	/**
	 * Override default Seam initialize() so that it reads in META-INF/pages.xml from all
	 * installed extensions (i.e. from all matching resources in the classpath).
	 */
	public void initialize(Set<FileDescriptor> fileNames) {
		pagesByViewId = Collections.synchronizedMap(new HashMap<String, Page>());   
		pageStacksByViewId = Collections.synchronizedMap(new HashMap<String, List<Page>>());   
		conversations = Collections.synchronizedMap(new HashMap<String, ConversationIdParameter>());
		
		//KIWI
		loginViewIds = new HashMap<String,String>();
		
		for (String resource: resources) 
		{
			InputStream stream = ResourceLoader.instance().getResourceAsStream(resource);      
			if (stream==null) 
			{
				log.info("no pages.xml file found: " + resource);
			} else {
				log.debug("reading pages.xml file: " + resource);
				try {
					parse("DEFAULT",stream);
				} finally {
					Resources.closeStream(stream);
				}
			}
		}

		if (fileNames != null)
		{
			parsePages(fileNames);
		}

		/* KIWI MODIFICATIONS BEGIN */
		Enumeration<URL> resources;
		try {
			resources = Thread.currentThread().getContextClassLoader().getResources("META-INF/pages.xml");
		} catch (IOException ioe) {
			throw new RuntimeException("error scanning META-INF/pages.xml files", ioe);
		}

		while (resources.hasMoreElements()) {
			URL url = resources.nextElement();
			try {
				String extName = getExtensionName(url);
				log.debug("reading path " + url + " for extension -" + extName + "-");
				parse(extName,url.openStream());
			} catch (Exception e) {
				throw new RuntimeException("error while reading " + url, e);
			}
		} 
		/* KIWI MODIFICATIONS END */
	}
	
	//KIWI get extension name from url
	private String getExtensionName(URL url) {
		String u = url.getPath();
		u = u.substring(u.indexOf("kiwiext")+8);
		u = u.substring(0,u.indexOf('.'));
		return u;
	}


	public static KiWiPages instance()
	{
		if ( !Contexts.isApplicationContextActive() )
		{
			throw new IllegalStateException("No active application context");
		}
		return (KiWiPages) Component.getInstance(KiWiPages.class, ScopeType.APPLICATION);
	}


	public void setResources(String[] resources)
	{
		/* KIWI MODIFICATIONS BEGIN */
		Set<String> set = new HashSet<String>();
		set.addAll(Arrays.asList(this.resources));
		set.addAll(Arrays.asList(resources));
		this.resources = set.toArray(this.resources);
		/* KIWI MODIFICATIONS END */
	}

	
	
	
	  private static final LogProvider log = Logging.getLogProvider(Pages.class);

	   /* KIWI MODIFICATION (there could be more login) */
	   //TODO alter noConversationId too
	   private ValueExpression<String> noConversationViewId;
	   private String loginViewId;
	   private HashMap<String,String> loginViewIds;
	   /* KIWI MODIFICATIONS END */
	   private Integer httpPort;
	   private Integer httpsPort;
	   
	   private Map<String, Page> pagesByViewId;  
	   private Map<String, List<Page>> pageStacksByViewId;   
	   private Map<String, ConversationIdParameter> conversations;    
	   
	   private String[] resources = { "/WEB-INF/pages.xml" };
	 
	   private SortedSet<String> wildcardViewIds = new TreeSet<String>( 
	         new Comparator<String>() 
	         {
	            public int compare(String x, String y)
	            {
	               if ( x.length()<y.length() ) return -1;
	               if ( x.length()> y.length() ) return 1;
	               return x.compareTo(y);
	            }
	         } 
	      );

	   @Create
	   public void create()
	   {
	      if (DotPageDotXmlDeploymentHandler.instance() != null)
	      {
	          initialize(DotPageDotXmlDeploymentHandler.instance().getResources());
	      }
	      else
	      {
	          initialize();
	      }
	   }
	   
	   
	   
	   public void initialize()
	   {
	       initialize(null);
	   }
	   
	   
	   private void parsePages(Set<FileDescriptor> files)
	   {
	      for (FileDescriptor file : files)  
	      {
	         String fileName = file.getName();
	         String viewId = "/" + fileName.substring(0,fileName.length()-".page.xml".length()) + ".xhtml"; // needs more here
	         
	         InputStream stream = null;
	         try
	         {
	            stream = file.getUrl().openStream();
	         }
	         catch (IOException exception)
	         {
	            // No-op
	         }
	         if (stream != null) 
	         {
	            log.debug("reading pages.xml file: " + fileName);
	            try {
	                parse(stream,viewId);
	            } finally {
	                Resources.closeStream(stream);
	            }
	         } 
	     }
	   }
	   
	   /**
	    * Run any navigation rule defined in pages.xml
	    * 
	    * @param actionExpression the action method binding expression
	    * @param actionOutcomeValue the outcome of the action method
	    * @return true if a navigation rule was found
	    */
	   public boolean navigate(FacesContext context, String actionExpression, String actionOutcomeValue)
	   {
	      String viewId = getViewId(context);
	      if (viewId!=null)
	      {
	         List<Page> stack = getPageStack(viewId);
	         for (int i=stack.size()-1; i>=0; i--)
	         {
	            Page page = stack.get(i);
	            Navigation navigation = page.getNavigations().get(actionExpression);
	            if (navigation==null)
	            {
	               navigation = page.getDefaultNavigation();
	            }
	            
	            if ( navigation!=null && navigation.navigate(context, actionOutcomeValue) ) return true;  
	            
	         }
	      }
	      return false;
	   }
	   /**
	    * Get the Page object for the given view id.
	    * 
	    * @param viewId a JSF view id
	    */
	   public Page getPage(String viewId)
	   {
	      if (viewId==null)
	      {
	         //for tests
	         return new Page(viewId);
	      }
	      else
	      {
	         Page result = getCachedPage(viewId);
	         if (result==null)
	         {
	            return createPage(viewId);
	         }
	         else
	         {
	            return result;
	         }
	      }
	   }
	   
	   /**
	    * Create a new default Page object for a JSF view id
	    */
	   private Page createPage(String viewId)
	   {
	      Page result = new Page(viewId);
	      pagesByViewId.put(viewId, result);
	      return result;
	   }
	   
	   private Page getCachedPage(String viewId)
	   {
	      return pagesByViewId.get(viewId);
	   }
	   
	   /**
	    * Get the stack of Page objects, from least specific to 
	    * most specific, that match the given view id.
	    * 
	    * @param viewId a JSF view id
	    */
	   protected List<Page> getPageStack(String viewId)
	   {
	      List<Page> stack = pageStacksByViewId.get(viewId);
	      if (stack==null)
	      {
	         stack = createPageStack(viewId);
	         pageStacksByViewId.put(viewId, stack);
	      }
	      return stack;
	   }
	   /**
	    * Create the stack of pages that match a JSF view id
	    */
	   private List<Page> createPageStack(String viewId)
	   {
	      List<Page> stack = new ArrayList<Page>(1);
	      if ( viewId!=null && !isDebugPage(viewId) )
	      {
	         for (String wildcard: wildcardViewIds)
	         {
	            if ( viewId.startsWith( wildcard.substring(0, wildcard.length()-1) ) )
	            {
	               stack.add( getPage(wildcard) );
	            }
	         }
	      }
	      Page page = getPage(viewId);
	      if (page!=null) stack.add(page);
	      return stack;
	   }
	   
	   /**
	    * Call page actions, check permissions and validate the existence 
	    * of a conversation for pages which require a long-running 
	    * conversation, starting with the most general view id, ending at 
	    * the most specific. Also perform redirection to the required
	    * scheme if necessary.
	    */
	   public boolean preRender(FacesContext facesContext)
	   {
	      String viewId = getViewId(facesContext);
	      
	      //redirect to HTTPS if necessary
	      String requestScheme = getRequestScheme(facesContext);
	      if ( requestScheme!=null )
	      {
	         String scheme = getScheme(viewId);
	         if ( scheme!=null && !requestScheme.equals(scheme) )
	         {
	            Manager.instance().redirect(viewId);
	            return false;
	         }
	      }
	      
	      //apply the datamodelselection passed by s:link or s:button
	      //before running any actions
	      selectDataModelRow(facesContext);

	      //redirect if necessary
	      List<Page> pageStack = getPageStack(viewId);
	      for ( Page page: pageStack )
	      {         
	         if ( isNoConversationRedirectRequired(page) )
	         {
	            redirectToNoConversationView();
	            return false;
	         }
	         else if ( isLoginRedirectRequired(viewId, page) )
	         {
	            redirectToLoginView();
	            return false;
	         }
	      }

	      boolean result = callAction(facesContext); 

	      //If responseComplete then we're probably doing a redirect so don't call the page actions now. 
	      if (!facesContext.getResponseComplete()) { 
	          String newViewId = getViewId(facesContext); 

	          for ( Page page: getPageStack(newViewId) ) { 
	              if ( isNoConversationRedirectRequired(page) ) { 
	                  redirectToNoConversationView(); 
	                  return false; 
	              } else if ( isLoginRedirectRequired(newViewId, page) ) { 
	                  redirectToLoginView(); 
	                  return false; 
	              } 
	          } 

	          //run the page actions, check permissions, 
	          //handle conversation begin/end 

	          for ( Page page: getPageStack(newViewId) ) { 
	              result = page.preRender(facesContext) || result; 
	          } 
	      } 
	      
	      return result;
	   }
	   
	   /**
	    * Look for a DataModel row selection in the request parameters,
	    * and apply it to the DataModel.
	    */
	   protected void selectDataModelRow(FacesContext facesContext)
	   {
	      String dataModelSelection = facesContext.getExternalContext()
	               .getRequestParameterMap().get("dataModelSelection");
	      if (dataModelSelection!=null)
	      {
	         int colonLoc = dataModelSelection.indexOf(':');
	         int bracketLoc = dataModelSelection.indexOf('[');
	         if (colonLoc>0 && bracketLoc>colonLoc)
	         {
	            String var = dataModelSelection.substring(0, colonLoc);
	            String name = dataModelSelection.substring(colonLoc+1, bracketLoc);
	            int index = Integer.parseInt( dataModelSelection.substring( bracketLoc+1, dataModelSelection.length()-1 ) );
	            Object value = Component.getInstance(name, true);
	            if (value!=null)
	            {
	               DataModel dataModel = (DataModel) value;
	               if ( index<dataModel.getRowCount() )
	               {
	                  dataModel.setRowIndex(index);
	                  Contexts.getEventContext().set( var, dataModel.getRowData() );
	               }
	               else
	               {
	                  log.warn("DataModel row was unavailable");
	                  Contexts.getEventContext().remove(var);
	               }
	            }
	         }
	      }
	   }
	   
	   /**
	    * Check permissions and validate the existence of a conversation
	    * for pages which require a long-running conversation, starting
	    * with the most general view id, ending at the most specific.
	    * Finally apply page parameters to the model.
	    */
	   public void postRestore(FacesContext facesContext)
	   {
	      //first store the page parameters into the viewroot, so 
	      //that if a login redirect occurs, or if a failure
	      //occurs while validating of applying to the model, we can 
	      //still make Redirect.captureCurrentView() work.
	      storeRequestStringValuesInPageContext(facesContext);
	      
	      //check if we need to redirect
	      String viewId = getViewId(facesContext);      
	      for ( Page page: getPageStack(viewId) )
	      {         
	         if ( isLoginRedirectRequired(viewId, page) )
	         {
	            redirectToLoginView();
	            return;
	         }
	         else if ( isNoConversationRedirectRequired(page) )
	         {
	            redirectToNoConversationView();
	            return;
	         }
	         else
	         {
	            //if we are about to proceed to the action
	            //phase, check the permission.
	            if ( !facesContext.getRenderResponse() )
	            {
	               page.postRestore(facesContext);
	            }
	         }
	      }

	      //now validate the values we just stored in
	      //the view root, after the redirect checking
	      if ( convertAndValidateStringValuesInPageContext(facesContext) ) 
	      {
	         Validation.instance().fail();
	         //and don't apply them to the model
	      }
	      else
	      {   
	         //finally apply page parameters to the model
	         //(after checking permissions)
	         applyConvertedValidatedValuesToModel(facesContext);
	      }
	   }
	   
	   /**
	    * Check if a login redirect is required for the current FacesContext
	    * 
	    * @param facesContext The faces context containing the view ID
	    * @return boolean Returns true if a login redirect is required
	    */
	   public boolean isLoginRedirectRequired(FacesContext facesContext)
	   {
	      String viewId = getViewId(facesContext);      
	      for ( Page page: getPageStack(viewId) )
	      {         
	         if ( isLoginRedirectRequired(viewId, page) ) return true;
	      }
	      return false;
	   }
	   
	   private boolean isNoConversationRedirectRequired(Page page)
	   {
	      return page.isConversationRequired() && 
	            !Manager.instance().isLongRunningOrNestedConversation();
	   }
	   
	   private boolean isLoginRedirectRequired(String viewId, Page page)
	   {
	      return page.isLoginRequired() && 
	            !viewId.equals( getLoginViewId() ) && 
	            !Identity.instance().isLoggedIn();
	   }
	   
	   public String getRequestScheme(FacesContext facesContext)
	   {
	      String requestUrl = getRequestUrl(facesContext);
	      if (requestUrl==null)
	      {
	         return null;
	      }
	      else
	      {
	         int idx = requestUrl.indexOf(':');
	         return idx<0 ? null : requestUrl.substring(0, idx);
	      }
	   }
	   
	   public String encodeScheme(String viewId, FacesContext context, String url)
	   {
	      String scheme = getScheme(viewId);
	      if (scheme != null)
	      {
	         String requestUrl = getRequestUrl(context);
	         if (requestUrl!=null)
	         {
	            try
	            {
	               URL serverUrl = new URL(requestUrl);
	               
	               StringBuilder sb = new StringBuilder();
	               sb.append(scheme);
	               sb.append("://");
	               sb.append(serverUrl.getHost());
	               
	               if ("http".equals(scheme) && httpPort != null)
	               {
	                  sb.append(":");
	                  sb.append(httpPort);
	               }
	               else if ("https".equals(scheme) && httpsPort != null)
	               {
	                  sb.append(":");
	                  sb.append(httpsPort);
	               }
	               else if (serverUrl.getPort() != -1)
	               {
	                  sb.append(":");
	                  sb.append(serverUrl.getPort());
	               }
	               
	               if (!url.startsWith("/")) sb.append("/");
	               
	               sb.append(url);
	               
	               url = sb.toString();
	            }
	            catch (MalformedURLException ex) 
	            {
	               throw new RuntimeException(ex);
	            }
	         }
	      }
	      return url;   
	   }
	   
	   private static String getRequestUrl(FacesContext facesContext)
	   {
	      Object request = facesContext.getExternalContext().getRequest(); 
	      if (request instanceof HttpServletRequest) 
	      {
	         return ( (HttpServletRequest) request).getRequestURL().toString();
	      }
	      else
	      {
	         return null;
	      }
	   }
	   
	   public void redirectToLoginView()
	   {
	      notLoggedIn();
	      
	      //get current Application
	      KiWiApplication ca = (KiWiApplication) Component.getInstance("currentApplication");
	      log.info(ca.getName());
	      String lvid = loginViewIds.get(ca.getName().toLowerCase());
	     
	      String loginViewId = getLoginViewId();
	      
	      if( lvid != null ) {
	    	  loginViewId = lvid;
	      }
	      if (loginViewId==null)
	      {
	         throw new NotLoggedInException();
	      }
	      else
	      {
	         Manager.instance().redirect(loginViewId);
	      }
	   }
	   
	   public void redirectToNoConversationView()
	   {
	      noConversation();
	      
	      //stuff from jPDL takes precedence
	      org.jboss.seam.faces.FacesPage facesPage = org.jboss.seam.faces.FacesPage.instance();
	      String pageflowName = facesPage.getPageflowName();
	      String pageflowNodeName = facesPage.getPageflowNodeName();
	      
	      String noConversationViewId = null;
	      if (pageflowName==null || pageflowNodeName==null)
	      {
	         String viewId = Pages.getCurrentViewId();
	         noConversationViewId = getNoConversationViewId(viewId);
	      }
	      else
	      {
	         noConversationViewId = Pageflow.instance().getNoConversationViewId(pageflowName, pageflowNodeName);
	      }
	      
	      if (noConversationViewId!=null)
	      {
	         Manager.instance().redirect(noConversationViewId);
	      }
	   }
	   
	   public String getScheme(String viewId)
	   {
	      List<Page> stack = getPageStack(viewId);
	      for ( int i = stack.size() - 1; i >= 0; i-- )
	      {
	         Page page = stack.get(i);
	         if (page.getScheme() != null) return page.getScheme();
	      }
	      return null;
	   }

	   public boolean hasDescription(String viewId)
	   {
	      return getDescription(viewId)!=null;
	   }

	   public String getDescription(String viewId)
	   {
	      List<Page> stack = getPageStack(viewId);
	      for ( int i = stack.size() - 1; i >= 0; i-- )
	      {
	         Page page = stack.get(i);
	         if (page.hasDescription()) return page.getDescription();
	      }
	      return null;
	   }

	   public String renderDescription(String viewId)
	   {
	      return Interpolator.instance().interpolate( getDescription(viewId) );
	   }
	   
	   protected void noConversation()
	   {
	      Events.instance().raiseEvent("org.jboss.seam.noConversation");
	      
	      FacesMessages.instance().addFromResourceBundleOrDefault( 
	            StatusMessage.Severity.WARN, 
	            "org.jboss.seam.NoConversation", 
	            "The conversation ended, timed out or was processing another request" 
	         );
	   }

	   protected void notLoggedIn()
	   {
	      //    TODO - Deprecated, remove for next major release
	      Events.instance().raiseEvent("org.jboss.seam.notLoggedIn");
	      Events.instance().raiseEvent(Identity.EVENT_NOT_LOGGED_IN);
	   }

	   public static String toString(Object returnValue)
	   {
	      return returnValue == null ? null : returnValue.toString();
	   }
	   
	   /**
	    * Call the JSF navigation handler
	    */
	   public static void handleOutcome(FacesContext facesContext, String outcome, String fromAction)
	   {
	      facesContext.getApplication().getNavigationHandler()
	            .handleNavigation(facesContext, fromAction, outcome);
	      //after every time that the view may have changed,
	      //we need to flush the page context, since the 
	      //attribute map is being discarder
	      Contexts.getPageContext().flush();
	   }
	   
	   /**
	    * Call the action requested by s:link or s:button.
	    */
	   private static boolean callAction(FacesContext facesContext)
	   {
	      //TODO: refactor with Pages.instance().callAction()!!
	      
	      boolean result = false;
	      
	      String outcome = facesContext.getExternalContext()
	            .getRequestParameterMap().get("actionOutcome");
	      String fromAction = outcome;
	      
	      if (outcome==null)
	      {
	         String actionId = facesContext.getExternalContext()
	               .getRequestParameterMap().get("actionMethod");
	         if (actionId!=null)
	         {
	            if ( !SafeActions.instance().isActionSafe(actionId) ) return result;
	            String expression = SafeActions.toAction(actionId);
	            result = true;
	            MethodExpression actionExpression = Expressions.instance().createMethodExpression(expression);
	            outcome = toString( actionExpression.invoke() );
	            fromAction = expression;
	            handleOutcome(facesContext, outcome, fromAction);
	         }
	      }
	      else
	      {
	         handleOutcome(facesContext, outcome, fromAction);
	      }
	      
	      return result;
	   }
	   
	   /**
	    * Build a list of page-scoped resource bundles, from most
	    * specific view id, to most general.
	    */
	   public List<ResourceBundle> getResourceBundles(String viewId)
	   {
	      List<ResourceBundle> result = new ArrayList<ResourceBundle>(1);
	      List<Page> stack = getPageStack(viewId);
	      for (int i=stack.size()-1; i>=0; i--)
	      {
	         Page page = stack.get(i);
	         ResourceBundle bundle = page.getResourceBundle();
	         if ( bundle!=null ) result.add(bundle);
	      }
	      return result;
	   }
	   
	   /**
	    * Get the values of any page parameters by evaluating the value bindings
	    * against the model and converting to String.
	    * 
	    * @param viewId the JSF view id
	    * @param overridden excluded parameters
	    * @return a map of page parameter name to String value
	    */
	   public Map<String, Object> getStringValuesFromModel(FacesContext facesContext, String viewId, Set<String> overridden)
	   {
	      Map<String, Object> parameters = new HashMap<String, Object>();
	      for ( Page page: getPageStack(viewId) )
	      {
	         for ( Param pageParameter: page.getParameters() )
	         {
	            if ( !overridden.contains( pageParameter.getName() ) )
	            {
	               String value;
	               if ( pageParameter.getValueExpression()==null )
	               {
	                  value = (String) Contexts.getPageContext().get( pageParameter.getName() );
	               }
	               else
	               {
	                  value = pageParameter.getStringValueFromModel(facesContext);
	               }
	               if (value!=null) 
	               {
	                  parameters.put( pageParameter.getName(), value );
	               }
	            }
	         }
	      }
	      return parameters;
	   }
	   
	   private void storeRequestStringValuesInPageContext(FacesContext facesContext)
	   {
	      Parameters parameters = Parameters.instance();
	      if (parameters!=null) //for unit tests
	      {
	         Map<String, String[]> requestParameters = parameters.getRequestParameters();
	         for ( Page page: getPageStack( getViewId(facesContext) ) )
	         {
	            for ( Param pageParameter: page.getParameters() )
	            {
	               String value = pageParameter.getStringValueFromRequest(facesContext, requestParameters);
	               if (value==null)
	               {
	                  //this should not be necessary, were it not for a MyFaces bug
	                  if ( facesContext.getRenderResponse() ) //ie. for a non-faces request
	                  {
	                     Contexts.getPageContext().remove( pageParameter.getName() );
	                  }
	               }
	               else
	               {
	                  Contexts.getPageContext().set( pageParameter.getName(), value );
	               }
	            }
	         }
	      }
	   }
	   
	   /**
	    * Convert and validate page parameters passed as view root attributes or request parameters
	    */
	   private boolean convertAndValidateStringValuesInPageContext(FacesContext facesContext)
	   {
	      boolean validationFailed = false;
	      for ( Page page: getPageStack( getViewId(facesContext) ) )
	      {
	         for ( Param pageParameter: page.getParameters() )
	         {  
	            try
	            {
	               String value = (String) Contexts.getPageContext().get( pageParameter.getName() );
	               if (value!=null)
	               {
	                  Object convertedValue = pageParameter.convertValueFromString(facesContext, value);
	                  pageParameter.validateConvertedValue(facesContext, convertedValue);
	                  Contexts.getEventContext().set( pageParameter.getName(), convertedValue );
	               }
	            }
	            catch (ValidatorException ve)
	            {
	               if (ve.getFacesMessage() != null)
	               {
	                  facesContext.addMessage(null, ve.getFacesMessage());
	               }
	               
	               validationFailed = true;
	            }
	            catch (ConverterException ce)
	            {
	               if (ce.getFacesMessage() != null)
	               {
	                  facesContext.addMessage( null, ce.getFacesMessage() );
	               }
	               validationFailed = true;
	            }
	         }
	      }
	      return validationFailed;
	   }
	   
	   /**
	    * Apply page parameters passed as view root attributes or request parameters to the model
	    */
	   private void applyConvertedValidatedValuesToModel(FacesContext facesContext)
	   {
	      String viewId = getViewId(facesContext);
	      for ( Page page: getPageStack(viewId) )
	      {
	         for ( Param pageParameter: page.getParameters() )
	         {         
	            ValueExpression valueExpression = pageParameter.getValueExpression();
	            if (valueExpression!=null)
	            {
	               Object object = Contexts.getEventContext().get( pageParameter.getName() );
	               if (object!=null)
	               {
	                  valueExpression.setValue(object);
	               }
	            }
	         }
	      }
	   }

	   /**
	    * Get the page parameter values that were passed in the original request from
	    * the PAGE context
	    */
	   public Map<String, Object> getStringValuesFromPageContext(FacesContext facesContext)
	   {
	      Map<String, Object> parameters = new HashMap<String, Object>();
	      String viewId = getViewId(facesContext);
	      for ( Page page: getPageStack(viewId) )
	      {
	         for ( Param pageParameter: page.getParameters() )
	         {
	            Object object = Contexts.getPageContext().get( pageParameter.getName() );
	            if (object!=null)
	            {
	               parameters.put( pageParameter.getName(), object );
	            }
	         }
	      }
	      return parameters;
	   }
	   
	   /**
	    * Update the page parameter values stored in the PAGE context with the current
	    * values of the mapped attributes of the model
	    */
	   public void updateStringValuesInPageContextUsingModel(FacesContext facesContext)
	   {
	      for ( Page page: getPageStack( getViewId(facesContext) ) )
	      {
	         for ( Param pageParameter: page.getParameters() )
	         {
	            if ( pageParameter.getValueExpression()!=null )
	            {
	               String value = pageParameter.getStringValueFromModel(facesContext);
	               if (value==null)
	               {
	                  Contexts.getPageContext().remove( pageParameter.getName() );
	               }
	               else
	               {
	                  Contexts.getPageContext().set( pageParameter.getName(), value );
	               }
	            }
	         }
	      }
	   }
	   
	   /**
	    * Encode page parameters into a URL
	    * 
	    * @param url the base URL
	    * @param viewId the JSF view id of the page
	    * @return the URL with parameters appended
	    */
	   public String encodePageParameters(FacesContext facesContext, String url, String viewId)
	   {
	      return encodePageParameters(facesContext, url, viewId, Collections.EMPTY_SET);
	   }
	   
	   /**
	    * Encode page parameters into a URL
	    * 
	    * @param url the base URL
	    * @param viewId the JSF view id of the page
	    * @param overridden excluded parameters
	    * @return the URL with parameters appended
	    */
	   public String encodePageParameters(FacesContext facesContext, String url, String viewId, Set<String> overridden)
	   {
	      Map<String, Object> parameters = getStringValuesFromModel(facesContext, viewId, overridden);
	      return Manager.instance().encodeParameters(url, parameters);
	   }
	   
	   /**
	    * Search for a defined no-conversation-view-id, beginning with
	    * the most specific view id, then wildcarded view ids, and 
	    * finally the global setting
	    */
	   public String getNoConversationViewId(String viewId)
	   {
	      List<Page> stack = getPageStack(viewId);
	      for (int i=stack.size()-1; i>=0; i--)
	      {
	         Page page = stack.get(i);
	         if (page.getNoConversationViewId() != null)
	         {
	            String noConversationViewId = page.getNoConversationViewId().getValue();
	            if (noConversationViewId!=null)
	            {
	               return noConversationViewId;
	            }
	         }
	      }
	      return this.noConversationViewId != null ? this.noConversationViewId.getValue() : null;
	   }
	   
	   /**
	    * Search for a defined conversation timeout, beginning with
	    * the most specific view id, then wildcarded view ids, and 
	    * finally the global setting from Manager
	    */
	   public Integer getTimeout(String viewId)
	   {
	      List<Page> stack = getPageStack(viewId);
	      for (int i=stack.size()-1; i>=0; i--)
	      {
	         Page page = stack.get(i);
	         Integer timeout = page.getTimeout();
	         if (timeout!=null)
	         {
	            return timeout;
	         }
	      }
	      return Manager.instance().getConversationTimeout();
	   }
	   
	   /**
	    * Search for a defined concurrent request timeout, beginning with
	    * the most specific view id, then wildcarded view ids, and 
	    * finally the global setting from Manager
	    */
	   public Integer getConcurrentRequestTimeout(String viewId)
	   {
	      List<Page> stack = getPageStack(viewId);
	      for (int i=stack.size()-1; i>=0; i--)
	      {
	         Page page = stack.get(i);
	         Integer concurrentRequestTimeout = page.getConcurrentRequestTimeout();
	         if (concurrentRequestTimeout!=null)
	         {
	            return concurrentRequestTimeout;
	         }
	      }
	      return Manager.instance().getConcurrentRequestTimeout();
	   }
	   
	   public static String getSuffix()
	   {
	      String defaultSuffix = FacesContext.getCurrentInstance().getExternalContext()
	            .getInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME);
	      return defaultSuffix == null ? ViewHandler.DEFAULT_SUFFIX : defaultSuffix;
	   }
	   
	   /**
	    * Parse a pages.xml file
	    */
	   private void parse(String extension,InputStream stream)
	   {
	      Element root = getDocumentRoot(stream);
	      if (noConversationViewId==null) //let the setting in components.xml override the pages.xml
	      {
	         String noConversationViewIdString = root.attributeValue("no-conversation-view-id");
	         if (noConversationViewIdString != null)
	         {
	            noConversationViewId = Expressions.instance().createValueExpression(noConversationViewIdString, String.class);
	         }
	      }
	      if (loginViewId==null && extension.equals("DEFAULT")) //let the setting in components.xml override the pages.xml
	      {
	         loginViewId = root.attributeValue("login-view-id");
	      }
	      
	      if( !extension.equals("DEFAULT") ) {
	    	  String lvid = root.attributeValue("login-view-id");
	    	  if( lvid != null ) {
	    		  loginViewIds.put(extension, lvid);
	    	  }
	      }
	      
	      if (httpPort == null)
	      {
	         try
	         {
	            String value = root.attributeValue("http-port");
	            if (!Strings.isEmpty(value))
	            {
	               httpPort = Integer.parseInt(value);
	            }
	         }
	         catch (NumberFormatException ex)
	         {
	            throw new IllegalStateException("Invalid value specified for http-port attribute in pages.xml");
	         }
	      }
	      
	      if (httpsPort == null)
	      {
	         try
	         {
	            String value = root.attributeValue("https-port");
	            if (!Strings.isEmpty(value))
	            {
	               httpsPort = Integer.parseInt(value);
	            }
	         }
	         catch (NumberFormatException ex)
	         {
	            throw new IllegalStateException("Invalid valid specified for https-port attribute in pages.xml");
	         }
	      }
	      
	      List<Element> elements = root.elements("conversation");
	      for (Element conversation : elements)
	      {
	         parseConversation(conversation, conversation.attributeValue("name"));
	      }
	      
	      elements = root.elements("page");
	      for (Element page: elements)
	      {
	         parse( page, page.attributeValue("view-id") );
	      } 
	   }
	   
	   /**
	    * Parse a viewId.page.xml file
	    */
	   private void parse(InputStream stream, String viewId)
	   {
	      parse( getDocumentRoot(stream), viewId );
	   }
	   
	   /**
	    * Get the root element of the document
	    */
	   private static Element getDocumentRoot(InputStream stream)
	   {
	      try
	      {
	         return XML.getRootElement(stream);
	      }
	      catch (DocumentException de)
	      {
	         throw new RuntimeException(de);
	      }
	   }
	   
	   private void parseConversation(Element element, String name)
	   {
	      if (name == null)
	      {
	         throw new IllegalStateException("Must specify name for <conversation/> declaration");
	      }
	      
	      if (conversations.containsKey(name))
	      {
	         throw new IllegalStateException("<conversation/> declaration already exists for [" + name + "]");
	      }
	      
	      NaturalConversationIdParameter param = new NaturalConversationIdParameter(name, 
	               element.attributeValue("parameter-name"), 
	               element.attributeValue("parameter-value"));
	      
	      conversations.put(name, param);
	   }
	   
	   /**
	    * Parse a page element and add a Page to the map
	    */
	   private void parse(Element element, String viewId)
	   {
	      if (viewId==null)
	      {
	         throw new IllegalStateException("Must specify view-id for <page/> declaration");
	      }
	      
	      if ( viewId.endsWith("*") )
	      {
	         wildcardViewIds.add(viewId);
	      }
	      Page page = new Page(viewId);
	      pagesByViewId.put(viewId, page);
	      
	      parsePage(page, element, viewId);
	      parseConversationControl( element, page.getConversationControl() );
	      parseTaskControl(element, page.getTaskControl());
	      parseProcessControl(element, page.getProcessControl());
	      List<Element> children = element.elements("param");
	      for (Element param: children)
	      {
	         page.getParameters().add( parseParam(param) );
	      }
	      
	      List<Element> moreChildren = element.elements("navigation");
	      for (Element fromAction: moreChildren)
	      {
	         parseActionNavigation(page, fromAction);
	      }
	      
	      Element restrict = element.element("restrict");
	      if (restrict != null)
	      {
	         page.setRestricted(true);
	         String expr = restrict.getTextTrim();
	         if ( !Strings.isEmpty(expr) ) page.setRestriction(expr);
	      }
	      
	      List<Element> headers = element.elements("header");
	      for (Element header: headers) {
	         page.getHeaders().add(parseHeader(header));
	      }
	   }
	   
	   public ConversationIdParameter getConversationIdParameter(String conversationName)
	   {
	      return conversations.get(conversationName);
	   }
	   
	   /**
	    * Parse the attributes of page
	    */
	   private Page parsePage(Page page, Element element, String viewId)
	   {
	      
	      page.setSwitchEnabled( !"disabled".equals( element.attributeValue("switch") ) );
	      
	      Element optionalElement = element.element("description");
	      String description = optionalElement==null ? 
	               element.getTextTrim() : optionalElement.getTextTrim();
	      if (description!=null && description.length()>0)
	      {
	         page.setDescription(description);
	      }
	      
	      String timeoutString = element.attributeValue("timeout");
	      if (timeoutString!=null)
	      {
	         page.setTimeout(Integer.parseInt(timeoutString));
	      }
	      
	      String concurrentRequestTimeoutString = element.attributeValue("concurrent-request-timeout");
	      if (concurrentRequestTimeoutString!=null)
	      {
	         page.setConcurrentRequestTimeout(Integer.parseInt(concurrentRequestTimeoutString));
	      }
	      
	      String noConversationViewIdString = element.attributeValue("no-conversation-view-id");
	      if (noConversationViewIdString != null)
	      {
	         page.setNoConversationViewId(Expressions.instance().createValueExpression(noConversationViewIdString, String.class));
	      }
	      page.setConversationRequired(Boolean.parseBoolean(element.attributeValue("conversation-required")));
	      page.setLoginRequired(Boolean.parseBoolean(element.attributeValue("login-required")));
	      page.setScheme(element.attributeValue("scheme"));
	      
	      String expiresValue = element.attributeValue("expires");
	      if (expiresValue != null) {
	           page.setExpires(Integer.parseInt(expiresValue));
	      }
	      
	      ConversationIdParameter param = conversations.get( element.attributeValue("conversation") );
	      if (param != null) page.setConversationIdParameter(param);
	      

	      List<Element> patterns = element.elements("rewrite");
	      for (Element pattern: patterns) {
	           page.addRewritePattern(pattern.attributeValue("pattern"));
	      }
	      
	      List<Element> events = element.elements("raise-event");
	      for (Element eventElement : events)
	      {
	         page.addEventType( eventElement.attributeValue("type") );
	      }
	      
	      Action action = parseAction(element, "action", false);
	      if (action!=null) page.getActions().add(action);
	      List<Element> childElements = element.elements("action");
	      for (Element childElement: childElements)
	      {
	         page.getActions().add( parseAction(childElement, "execute", true) );
	      }
	            
	      String bundle = element.attributeValue("bundle");
	      if (bundle!=null)
	      {
	         page.setResourceBundleName(bundle);
	      }
	      List<Element> moreChildElements = element.elements("in");
	      for (Element child: moreChildElements)
	      {
	         Input input = new Input();
	         input.setName( child.attributeValue("name") );
	         input.setValue( Expressions.instance().createValueExpression( child.attributeValue("value") ) );
	         String scopeName = child.attributeValue("scope");
	         if (scopeName!=null)
	         {
	            input.setScope( ScopeType.valueOf( scopeName.toUpperCase() ) );
	         }
	         page.getInputs().add(input);
	      }
	      
	      return page;
	   }
	   
	   private static Action parseAction(Element element, String actionAtt, boolean conditionalsAllowed)
	   {
	      Action action = new Action();
	      String methodExpression = element.attributeValue(actionAtt);
	      if (methodExpression==null) return null;
	      if ( methodExpression.startsWith("#{") )
	      {
	         action.setMethodExpression( Expressions.instance().createMethodExpression(methodExpression) );
	      }
	      else
	      {
	         action.setOutcome(methodExpression);
	      }
	      
	      if (conditionalsAllowed)
	      {
	         String expression = element.attributeValue("if");
	         if (expression!=null)
	         {
	            action.setValueExpression( Expressions.instance().createValueExpression(expression) );
	         }
	         action.setOnPostback(!"false".equals(element.attributeValue("on-postback")));
	      }
	      return action;
	   }
	   
	   /**
	    * Parse end-conversation (and end-task) and begin-conversation (start-task and begin-task) 
	    *
	    */
	   private static void parseConversationControl(Element element, ConversationControl control)
	   {
	      Element endConversation = element.element("end-conversation");
	      endConversation = endConversation == null ? element.element("end-task") : endConversation;
	      if ( endConversation!=null )
	      {
	         control.setEndConversation(true);
	         control.setEndConversationBeforeRedirect( Boolean.parseBoolean( endConversation.attributeValue("before-redirect") ) );
	         control.setEndRootConversation( Boolean.parseBoolean( endConversation.attributeValue("root") ) );
	         String expression = endConversation.attributeValue("if");
	         if (expression!=null)
	         {
	            control.setEndConversationCondition( Expressions.instance().createValueExpression(expression, Boolean.class) );
	         }
	      }
	      
	      Element beginConversation = element.element("begin-conversation");
	      beginConversation = beginConversation == null ? element.element("begin-task") : beginConversation;
	      beginConversation = beginConversation == null ? element.element("start-task") : beginConversation;
	      if ( beginConversation!=null )
	      {
	         control.setBeginConversation(true);
	         control.setJoin( Boolean.parseBoolean( beginConversation.attributeValue("join") ) );
	         control.setNested( Boolean.parseBoolean( beginConversation.attributeValue("nested") ) );
	         control.setPageflow( beginConversation.attributeValue("pageflow") );
	         control.setConversationName( beginConversation.attributeValue("conversation") );
	         String flushMode = beginConversation.attributeValue("flush-mode");
	         if (flushMode!=null)
	         {
	            control.setFlushMode( FlushModeType.valueOf( flushMode.toUpperCase() ) );
	         }
	         String expression = beginConversation.attributeValue("if");
	         if (expression!=null)
	         {
	            control.setBeginConversationCondition( Expressions.instance().createValueExpression(expression, Boolean.class) );
	         }
	      }
	      
	      if ( control.isBeginConversation() && control.isEndConversation() )
	      {
	         throw new IllegalStateException("cannot use both <begin-conversation/> and <end-conversation/>");
	      }
	   }
	   
	   /**
	    * Parse begin-task, start-task and end-task
	    */
	   private static void parseTaskControl(Element element, TaskControl control)
	   {
	      Element endTask = element.element("end-task");
	      if ( endTask!=null )
	      {
	         control.setEndTask(true);
	         String transition = endTask.attributeValue("transition");
	         if (transition != null)
	         {
	            control.setTransition( Expressions.instance().createValueExpression(transition, String.class) );
	         }
	      }
	      
	      Element beginTask = element.element("begin-task");
	      if ( beginTask!=null )
	      {
	         control.setBeginTask(true);
	         String taskId = beginTask.attributeValue("task-id");
	         if (taskId==null)
	         {
	           taskId = "#{param.taskId}";
	         }
	         control.setTaskId( Expressions.instance().createValueExpression(taskId, Long.class) );
	      }
	      
	      Element startTask = element.element("start-task");
	      if ( startTask!=null )
	      {
	         control.setStartTask(true);
	         String taskId = startTask.attributeValue("task-id");
	         if (taskId==null)
	         {
	           taskId = "#{param.taskId}";
	         }
	         control.setTaskId( Expressions.instance().createValueExpression(taskId, Long.class) );
	      }
	      
	      if ( control.isBeginTask() && control.isEndTask() )
	      {
	         throw new IllegalStateException("cannot use both <begin-task/> and <end-task/>");
	      }
	      else if ( control.isBeginTask() && control.isStartTask() )
	      {
	          throw new IllegalStateException("cannot use both <start-task/> and <begin-task/>");
	       }
	      else if ( control.isStartTask() && control.isEndTask() )
	      {
	           throw new IllegalStateException("cannot use both <start-task/> and <end-task/>");
	       }
	   }
	   
	   /**
	    * Parse create-process and end-process
	    */
	   private static void parseProcessControl(Element element, ProcessControl control)
	   {
	      Element createProcess = element.element("create-process");
	      if ( createProcess!=null )
	      {
	         control.setCreateProcess(true);
	         control.setDefinition( createProcess.attributeValue("definition") );
	      }
	      
	      Element resumeProcess = element.element("resume-process");
	      if ( resumeProcess!=null )
	      {
	         control.setResumeProcess(true);
	         String processId = resumeProcess.attributeValue("process-id");
	         if (processId==null)
	         {
	           processId = "#{param.processId}";
	         }
	         control.setProcessId( Expressions.instance().createValueExpression(processId, Long.class) );
	      }
	      
	      if ( control.isCreateProcess() && control.isResumeProcess() )
	      {
	         throw new IllegalStateException("cannot use both <create-process/> and <resume-process/>");
	      }
	   }
	   
	   private static void parseEvent(Element element, Rule rule)
	   {
	      List<Element> events = element.elements("raise-event");
	      for (Element eventElement : events)
	      {
	         rule.addEventType( eventElement.attributeValue("type") );
	      }
	   }
	   
	   /**
	    * Parse navigation
	    */
	   private static void parseActionNavigation(Page entry, Element element)
	   {
	      Navigation navigation = new Navigation(); 
	      String outcomeExpression = element.attributeValue("evaluate");
	      if (outcomeExpression!=null)
	      {
	         navigation.setOutcome( Expressions.instance().createValueExpression(outcomeExpression) );
	      }
	      
	      List<Element> cases = element.elements("rule");
	      for (Element childElement: cases)
	      {
	         navigation.getRules().add( parseRule(childElement) );
	      }
	      
	      Rule rule = new Rule();
	      parseEvent(element, rule);
	      parseNavigationHandler(element, rule);
	      parseConversationControl( element, rule.getConversationControl() );
	      parseTaskControl(element, rule.getTaskControl());
	      parseProcessControl(element, rule.getProcessControl());
	      navigation.setRule(rule);
	      
	      String expression = element.attributeValue("from-action");
	      if (expression==null)
	      {
	         if (entry.getDefaultNavigation()==null)
	         {
	            entry.setDefaultNavigation(navigation);
	         }
	         else
	         {
	            throw new IllegalStateException("multiple catchall <navigation> elements");
	         }
	      }
	      else
	      {
	         Object old = entry.getNavigations().put(expression, navigation);
	         if (old!=null)
	         {
	            throw new IllegalStateException("multiple <navigation> elements for action: " + expression);
	         }
	      }
	   }
	   
	   /**
	    * Parse param
	    */
	   private static Param parseParam(Element element)
	   {
	      String valueExpression = element.attributeValue("value");
	      String name = element.attributeValue("name");
	      if (name==null)
	      {
	         if (valueExpression==null)
	         {
	            throw new IllegalArgumentException("must specify name or value for page <param/> declaration");
	         }
	         name = valueExpression.substring(2, valueExpression.length()-1);
	      }
	      Param param = new Param(name);
	      if (valueExpression!=null)
	      {
	         param.setValueExpression(Expressions.instance().createValueExpression(valueExpression));
	      }
	      param.setConverterId(element.attributeValue("converterId"));
	      String converterExpression = element.attributeValue("converter");
	      if (converterExpression!=null)
	      {
	         param.setConverterValueExpression(Expressions.instance().createValueExpression(converterExpression));
	      }
	      param.setValidatorId(element.attributeValue("validatorId"));
	      String validatorExpression = element.attributeValue("validator");
	      if (validatorExpression!=null)
	      {
	         param.setValidatorValueExpression(Expressions.instance().createValueExpression(validatorExpression));
	      }
	      param.setRequired( Boolean.parseBoolean( element.attributeValue("required") ) );
	      return param;
	   }
	   
	   private static Header parseHeader(Element element)
	   {
	       Header header = new Header();

	       String name = element.attributeValue("name");
	       header.setName(name);

	       String valueExpression = element.attributeValue("value");
	       if (valueExpression==null) {
	           valueExpression = element.getTextTrim();
	       }
	       header.setValue(Expressions.instance().createValueExpression(valueExpression));

	       return header;
	   }
	   
	   /**
	    * Parse rule
	    */
	   private static Rule parseRule(Element element)
	   {
	      Rule rule = new Rule();
	      
	      rule.setOutcomeValue( element.attributeValue("if-outcome") );
	      String expression = element.attributeValue("if");
	      if (expression!=null)
	      {
	         rule.setCondition( Expressions.instance().createValueExpression(expression)  );
	      }
	      
	      parseConversationControl( element, rule.getConversationControl() );
	      parseTaskControl(element, rule.getTaskControl());
	      parseProcessControl(element, rule.getProcessControl());
	      parseEvent(element, rule);
	      parseNavigationHandler(element, rule);
	      
	      return rule;
	   }
	   
	   private static void parseNavigationHandler(Element element, Rule rule)
	   {
	      
	      Element render = element.element("render");
	      if (render!=null)
	      {
	         final String viewId = render.attributeValue("view-id");
	         Element messageElement = render.element("message");
	         String message = messageElement==null ? null : messageElement.getTextTrim();
	         String control = messageElement==null ? null : messageElement.attributeValue("for");
	         String severityName = messageElement==null ? null : messageElement.attributeValue("severity");
	         Severity severity = severityName==null ? 
	                  FacesMessage.SEVERITY_INFO : 
	                  getFacesMessageValuesMap().get( severityName.toUpperCase() );
	         rule.addNavigationHandler( new RenderNavigationHandler(stringValueExpressionFor(viewId), message, severity, control) );
	      }
	      
	      Element redirect = element.element("redirect");
	      if (redirect!=null)
	      {
	         List<Element> children = redirect.elements("param");
	         final List<Param> params = new ArrayList<Param>();
	         for (Element child: children)
	         {
	            params.add( parseParam(child) );
	         }
	         final String viewId = redirect.attributeValue("view-id");
	         final String url    = redirect.attributeValue("url");
	         final String includePageParamsAttr = redirect.attributeValue("include-page-params");
	         final boolean includePageParams = includePageParamsAttr == null ? true : Boolean.getBoolean(includePageParamsAttr);

	         Element messageElement = redirect.element("message");
	         String control = messageElement==null ? null : messageElement.attributeValue("for");
	         String message = messageElement==null ? null : messageElement.getTextTrim();
	         String severityName = messageElement==null ? null : messageElement.attributeValue("severity");
	         Severity severity = severityName==null ? 
	                  FacesMessage.SEVERITY_INFO : 
	                  getFacesMessageValuesMap().get( severityName.toUpperCase() );
	         rule.addNavigationHandler(new RedirectNavigationHandler(stringValueExpressionFor(viewId), 
	               stringValueExpressionFor(url), params, message, severity, control, includePageParams) );
	      }
	      
	      List<Element> childElements = element.elements("out");
	      for (Element child: childElements)
	      {
	         Output output = new Output();
	         output.setName( child.attributeValue("name") );
	         output.setValue( Expressions.instance().createValueExpression( child.attributeValue("value") ) );
	         String scopeName = child.attributeValue("scope");
	         if (scopeName==null)
	         {
	            output.setScope(ScopeType.CONVERSATION);
	         }
	         else
	         {
	            output.setScope( ScopeType.valueOf( scopeName.toUpperCase() ) );
	         }
	         rule.getOutputs().add(output);
	      }
	      
	   }
	   
	   private static ValueExpression<String> stringValueExpressionFor(String expr) {
	       return (ValueExpression<String>) ((expr == null) ? expr : Expressions.instance().createValueExpression(expr, String.class));
	   }
	   
	   public static Map<String, Severity> getFacesMessageValuesMap()
	   {
	      Map<String, Severity> result = new HashMap<String, Severity>();
	      for (Map.Entry<String, Severity> me: (Set<Map.Entry<String, Severity>>) FacesMessage.VALUES_MAP.entrySet())
	      {
	         result.put( me.getKey().toUpperCase(), me.getValue() );
	      }
	      return result;
	   }
	   
	   /**
	    * The global setting for no-conversation-viewid.
	    * 
	    * @return a JSF view id
	    */
	   public ValueExpression<String> getNoConversationViewId()
	   {
	      return noConversationViewId;
	   }
	   
	   public void setNoConversationViewId(ValueExpression<String> noConversationViewId)
	   {
	      this.noConversationViewId = noConversationViewId;
	   }
	   
	   /**
	    * The global setting for login-viewid.
	    * 
	    * @return a JSF view id
	    */
	   public String getLoginViewId()
	   {
	      return loginViewId;
	   }
	   
	   public void setLoginViewId(String loginViewId)
	   {
	      this.loginViewId = loginViewId;
	   }
	   
	   public static String getCurrentViewId()
	   {
	      return getViewId( FacesContext.getCurrentInstance() );
	   }
	   
	   public static String getCurrentBaseName()
	   {
	      String viewId = getViewId(FacesContext.getCurrentInstance());

	      int pos = viewId.lastIndexOf("/");
	      if (pos != -1)
	      {
	         viewId = viewId.substring(pos + 1);
	      }

	      pos = viewId.lastIndexOf(".");
	      if (pos != -1)
	      {
	         viewId = viewId.substring(0, pos);
	      }

	      return viewId;      
	   }   
	   
	   public static String getViewId(FacesContext facesContext)
	   {
	      if (facesContext!=null)
	      {
	         UIViewRoot viewRoot = facesContext.getViewRoot();
	         if (viewRoot!=null) return viewRoot.getViewId();
	      }
	      return null;
	   }
	   
	   public Integer getHttpPort()
	   {
	      return httpPort;
	   }
	   
	   public void setHttpPort(Integer httpPort)
	   {
	      this.httpPort = httpPort;
	   }
	   
	   public Integer getHttpsPort()
	   {
	      return httpsPort;
	   }
	   
	   public void setHttpsPort(Integer httpsPort)
	   {
	      this.httpsPort = httpsPort;
	   }
	   
	   public String[] getResources()
	   {
	      return resources;
	   }
	   
	   private static boolean isDebugPage(String viewId)
	   {
	      return Init.instance().isDebugPageAvailable() && viewId.startsWith("/debug.");
	   }
	   
	   public static boolean isDebugPage()
	   {
	      return Init.instance().isDebugPageAvailable() &&
	            getCurrentViewId() != null &&
	            getCurrentViewId().startsWith("/debug.");
	   }
	   
	   public Collection<String> getKnownViewIds() {
	       return pagesByViewId.keySet();
	   }

}
